package jastgen.frontend;

import java.io.*;
import java.util.*;

import jastgen.CompileException;
import jastgen.frontend.ByCaper.Token;
import static jastgen.frontend.Unit.*;
import static jastgen.frontend.ByCaper.Token.*;

public class Scanner {

	// 識別子ではないもの
	private static final Map<String, Token>
	nonIDs = new HashMap<String, Token>();

	static {
		nonIDs.put("package", PACKAGE);
		nonIDs.put("import", IMPORT);
		nonIDs.put("static", STATIC);
		nonIDs.put("interface", INTERFACE);
		nonIDs.put("*", ASTER);
		nonIDs.put(",", COMMA);
		nonIDs.put(".", DOT);
		nonIDs.put(":", COLON);
		nonIDs.put(";", SEMICOLON);
		nonIDs.put("[", BRACKETL);
		nonIDs.put("]", BRACKETR);
		nonIDs.put("{", BRACEL);
		nonIDs.put("}", BRACER);
		nonIDs.put("!", EXCL);
		nonIDs.put("<", LT);
		nonIDs.put(">", GT);
	}

	private final PushbackReader chars;
	private int lineNum;

	public Scanner(final String inputPath) throws IOException {

		chars = new PushbackReader(
			new BufferedReader(new FileReader(inputPath)));
	}

	public void close() throws IOException {

		chars.close();
	}

	public int getLineNum() {
		return lineNum;
	}

	public Unit getNextToken() throws CompileException {
		try {
			return getNextTokenImpl();
		} catch(final IOException e) {
			throw new CompileException(e);
		}
	}

	public Unit getNextTokenImpl() throws CompileException, IOException {

		// 空白文字を無視
		int next;
		do {
			next = chars_read();
		} while(isWhiteSpace(next));

		// コメントを無視
		if(next == '/') {
			next = chars_read();
			if(next == '/') {
				do {
					next = chars_read();
				} while(next != '\n' && next != '\r' && next != -1);
				return getNextToken();
			}
			if(next == '*') {
				while(true) {
					do {
						next = chars_read();
						if(next == -1)
							throw new CompileException(
								"Block comment wasn't closed! (in line " + lineNum + ")");
					} while(next != '*');
					next = chars_read();
					if(next == '/')
						return getNextToken();
				}
			}
		}

		// 「%{...}%」はMETHODBODY
		if(next == '%') {
			next = chars_read();
			if(next == '{') {
				final StringBuilder body = new StringBuilder();
				body.append("%{");

				final int lineNum_ = lineNum;

				boolean maybeLast = false;
				while(true) {

					next = chars_read();
					if(next == -1)
						throw new CompileException(
							"Method body wasn't closed! (in line " + lineNum_ + ")");
					body.appendCodePoint(next);

					if(maybeLast && next == '%')
						return unit(METHODBODY, body.toString());

					maybeLast = false;

					if(next == '}')
						maybeLast = true;
				}

			}
			return throwException(next);
		}

		if(isAlpha(next)) {  // 識別子と予約語

			final StringBuilder str = new StringBuilder();

			do {
				str.appendCodePoint(next);
				next = chars_read();
			} while(isAlpha(next) || isDigit(next) || next == '_');

			chars.unread(next);

			final String symbol = str.toString();
			return checkTokenType(symbol);

		} else if(next == '*') {
			return unit(ASTER, "*");
		} else if(next == ',') {
			return unit(COMMA, ",");
		} else if(next == '.') {
			return unit(DOT, ".");
		} else if(next == ':') {
			return unit(COLON, ":");
		} else if(next == ';') {
			return unit(SEMICOLON, ";");
		} else if(next == '[') {
			return unit(BRACKETL, "[");
		} else if(next == ']') {
			return unit(BRACKETR, "]");
		} else if(next == '{') {
			return unit(BRACEL, "{");
		} else if(next == '}') {
			return unit(BRACER, "}");
		} else if(next == '!') {
			return unit(EXCL, "!");
		} else if(next == '<') {
			return unit(LT, "<");
		} else if(next == '>') {
			return unit(GT, ">");
		} else if(next == -1) {
			return unit(eof, null);
		}

		return throwException(next);
	}

	private Unit throwException(
			final int next
		) throws CompileException {
		throw new CompileException(
			"Invalid token \""+Character.toChars(next)[0]+"\" was found. (in line " + lineNum + ")");  // FIXME: サロゲートペアを適切に扱う
	}

	// isSkippedChar
	private boolean isWhiteSpace(final int codePoint) {

		/* 未検証
		return (codePoint==' '
		     || codePoint=='\t'
		     || codePoint=='\n'
		     || codePoint=='\f'
		     || codePoint=='\r');
		*/
		return Character.isWhitespace(codePoint);
		// return Character.isSpaceChar(codePoint);  // 改行を識別不可?
	}

	private boolean isAlpha(final int codePoint) {
		return (('a'<=codePoint && codePoint<='z')
		     || ('A'<=codePoint && codePoint<='Z'));
	}

	private boolean isDigit(final int codePoint) {
		return ('0'<=codePoint && codePoint<='9');
	}

	private Unit checkTokenType(final String word) {

		if(nonIDs.containsKey(word))
			return unit(nonIDs.get(word), word);

		return unit(IDENTIFIER, word);
	}

	private int chars_read() throws IOException {
		final int c = chars.read();
		if(c == '\n')
			++lineNum;
		return c;
	}
}
